﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Xml;
using WooCoo.Tools;

namespace WooCoo.DataPortal.Client
{
    public static class ClientConvert
    {
#if !ASTORIA_LIGHT // System.Data.Linq not available
        /// <summary>fullname for assembly</summary>
        //private const string SystemDataLinq = "System.Data.Linq, Version=" + FXAssembly.Version + ", Culture=neutral, PublicKeyToken=" + AssemblyRef.EcmaPublicKeyToken;
        private const string SystemDataLinq = "System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
#endif

        /// <summary>list of known value (and reference) types</summary>
        /// <remarks>
        /// examples of unsupported value types
        /// IntPtr, UIntPtr, byte*, char*
        /// </remarks>
        private static readonly Type[] knownTypes = CreateKnownPrimitives();

#if !ASTORIA_LIGHT // System.Data.Linq not available
        /// <summary>has System.Data.Link.Binary not been delay loaded yet</summary>
        private static bool needSystemDataLinqBinary = true;
#endif

        /// <summary>StorageType enum</summary>
        internal enum StorageType
        {
            /// <summary>Boolean</summary>
            Boolean,

            /// <summary>Byte</summary>
            Byte,

            /// <summary>ByteArray</summary>
            ByteArray,

            /// <summary>Char</summary>
            Char,

            /// <summary>CharArray</summary>
            CharArray,

            /// <summary>DateTime</summary>
            DateTime,

            /// <summary>DateTimeOffset</summary>
            DateTimeOffset,

            /// <summary>Decimal</summary>
            Decimal,

            /// <summary>Double</summary>
            Double,

            /// <summary>Guid</summary>
            Guid,

            /// <summary>Int16</summary>
            Int16,

            /// <summary>Int32</summary>
            Int32,

            /// <summary>Int64</summary>
            Int64,

            /// <summary>Single</summary>
            Single,

            /// <summary>String</summary>
            String,

            /// <summary>SByte</summary>
            SByte,

            /// <summary>TimeSpan</summary>
            TimeSpan,

            /// <summary>Type</summary>
            Type,

            /// <summary>UInt16</summary>
            UInt16,

            /// <summary>UInt32</summary>
            UInt32,

            /// <summary>UInt64</summary>
            UInt64,

            /// <summary>Uri</summary>
            Uri,

            /// <summary>System.Xml.Linq.XDocument</summary>
            XDocument,

            /// <summary>System.Xml.Linq.XElement</summary>
            XElement,

#if !ASTORIA_LIGHT // System.Data.Linq not available
            /// <summary>System.Data.Linq.Binary</summary>
            Binary,
#endif
        }

        /// <summary>
        /// convert from string to the appropriate type
        /// </summary>
        /// <param name="propertyValue">incoming string value</param>
        /// <param name="propertyType">type to convert to</param>
        /// <returns>converted value</returns>
        internal static object ChangeType(string propertyValue, Type propertyType)
        {
            Debug.Assert(null != propertyValue, "should never be passed null");
            try
            {
                switch ((StorageType)IndexOfStorage(propertyType))
                {
                    case StorageType.Boolean:
                        return XmlConvert.ToBoolean(propertyValue);
                    case StorageType.Byte:
                        return XmlConvert.ToByte(propertyValue);
                    case StorageType.ByteArray:
                        return Convert.FromBase64String(propertyValue);
                    case StorageType.Char:
                        return XmlConvert.ToChar(propertyValue);
                    case StorageType.CharArray:
                        return propertyValue.ToCharArray();
                    case StorageType.DateTime:
                        return XmlConvert.ToDateTime(propertyValue, XmlDateTimeSerializationMode.RoundtripKind);
                    case StorageType.DateTimeOffset:
                        return XmlConvert.ToDateTimeOffset(propertyValue);
                    case StorageType.Decimal:
                        return XmlConvert.ToDecimal(propertyValue);
                    case StorageType.Double:
                        return XmlConvert.ToDouble(propertyValue);
                    case StorageType.Guid:
                        return new Guid(propertyValue);
                    case StorageType.Int16:
                        return XmlConvert.ToInt16(propertyValue);
                    case StorageType.Int32:
                        return XmlConvert.ToInt32(propertyValue);
                    case StorageType.Int64:
                        return XmlConvert.ToInt64(propertyValue);
                    case StorageType.Single:
                        return XmlConvert.ToSingle(propertyValue);
                    case StorageType.String:
                        return propertyValue;
                    case StorageType.SByte:
                        return XmlConvert.ToSByte(propertyValue);
                    case StorageType.TimeSpan:
                        return XmlConvert.ToTimeSpan(propertyValue);
                    case StorageType.Type:
                        return Type.GetType(propertyValue, true);
                    case StorageType.UInt16:
                        return XmlConvert.ToUInt16(propertyValue);
                    case StorageType.UInt32:
                        return XmlConvert.ToUInt32(propertyValue);
                    case StorageType.UInt64:
                        return XmlConvert.ToUInt64(propertyValue);
                    case StorageType.Uri:
                        return UriHelper.CreateUri(propertyValue, UriKind.RelativeOrAbsolute);
                    case StorageType.XDocument:
                        return (0 < propertyValue.Length ? System.Xml.Linq.XDocument.Parse(propertyValue) : new System.Xml.Linq.XDocument());
                    case StorageType.XElement:
                        return System.Xml.Linq.XElement.Parse(propertyValue);
#if !ASTORIA_LIGHT // System.Data.Linq not available
                    case StorageType.Binary:
                        Debug.Assert(null != knownTypes[(int)StorageType.Binary], "null typeof(System.Data.Linq.Binary)");
                        return Activator.CreateInstance(knownTypes[(int)StorageType.Binary], Convert.FromBase64String(propertyValue));
#endif
                    default:
                        Debug.Assert(false, "new StorageType without update to knownTypes");
                        return propertyValue;
                }
            }
            catch (FormatException ex)
            {
                propertyValue = (0 == propertyValue.Length ? "String.Empty" : "String");
                throw Error.InvalidOperation(StringID.CantDeserialize.ToLocalStringFormat(propertyType.ToString(), propertyValue), ex);
            }
            catch (OverflowException ex)
            {
                propertyValue = (0 == propertyValue.Length ? "String.Empty" : "String");
                throw Error.InvalidOperation(StringID.CantDeserialize.ToLocalStringFormat(propertyType.ToString(), propertyValue), ex);
            }
        }

        /// <summary>Determines whether the specified value is a System.Data.Linq.Binary value.</summary>
        /// <param name="value">Value to check.</param>
        /// <returns>true if the value is a System.Data.Linq.Binary value; false otherwise.</returns>
        internal static bool IsBinaryValue(object value)
        {
            Debug.Assert(value != null, "value != null");
            return StorageType.Binary == (StorageType)IndexOfStorage(value.GetType());
        }

        /// <summary>
        /// Convert from primitive value to an xml payload string. 
        /// NOTE: We need to pay special attention to DateTimes - if the converted value is going to be used as a content of 
        /// atom:updated or atom:published element we have to ensure it contains information about time zone. At the same time we 
        /// must not touch datetime values that in content or are mapped to custom elements. 
        /// </summary>
        /// <param name="propertyValue">incoming object value</param>
        /// <param name="atomDateConstruct">whether the converted value is going to be used as a content of an atom 
        /// element of atomDateConstruct type (e.g. atom:updated or atom:published)</param>
        /// <returns>converted value</returns>
        internal static string ToString(object propertyValue, bool atomDateConstruct)
        {
            Debug.Assert(null != propertyValue, "null should be handled by caller");
            switch ((StorageType)IndexOfStorage(propertyValue.GetType()))
            {
                case StorageType.Boolean:
                    return XmlConvert.ToString((bool)propertyValue);
                case StorageType.Byte:
                    return XmlConvert.ToString((byte)propertyValue);
                case StorageType.ByteArray:
                    return Convert.ToBase64String((byte[])propertyValue);
                case StorageType.Char:
                    return XmlConvert.ToString((char)propertyValue);
                case StorageType.CharArray:
                    return new String((char[])propertyValue);
                case StorageType.DateTime:
                    DateTime dt = (DateTime)propertyValue;
                    /* If the dt.Kind == DateTimeKind.Unspecified XmlConvert will not add information about timezone to the resulting datetime 
                       string. For atom:published and atom:updated elements information about timezone is required otherwise the message is not 
                       a valid atom message. To get the information about timezone we need to make datetimes of Unspecified kind become UniversalTime.
                       We do it the same way as we do it in JsonWriter class. 
                       WE MUST NOT TOUCH THE DATE IF IT IS NOT GOING TO BE USED FOR atom:updated OR atom:published ELEMENTS (i.e. isForAtomElement == false). */
                    return XmlConvert.ToString(dt.Kind == DateTimeKind.Unspecified && atomDateConstruct ? new DateTime(dt.Ticks, DateTimeKind.Utc) : dt, XmlDateTimeSerializationMode.RoundtripKind);
                case StorageType.DateTimeOffset:
                    return XmlConvert.ToString((DateTimeOffset)propertyValue);
                case StorageType.Decimal:
                    return XmlConvert.ToString((Decimal)propertyValue);
                case StorageType.Double:
                    return XmlConvert.ToString((Double)propertyValue);
                case StorageType.Guid:
                    return ((Guid)propertyValue).ToString();
                case StorageType.Int16:
                    return XmlConvert.ToString((Int16)propertyValue);
                case StorageType.Int32:
                    return XmlConvert.ToString((Int32)propertyValue);
                case StorageType.Int64:
                    return XmlConvert.ToString((Int64)propertyValue);
                case StorageType.Single:
                    return XmlConvert.ToString((Single)propertyValue);
                case StorageType.String:
                    return (String)propertyValue;
                case StorageType.SByte:
                    return XmlConvert.ToString((SByte)propertyValue);
                case StorageType.TimeSpan:
                    return XmlConvert.ToString((TimeSpan)propertyValue);
                case StorageType.Type:
                    return ((Type)propertyValue).AssemblyQualifiedName;
                case StorageType.UInt16:
                    return XmlConvert.ToString((UInt16)propertyValue);
                case StorageType.UInt32:
                    return XmlConvert.ToString((UInt32)propertyValue);
                case StorageType.UInt64:
                    return XmlConvert.ToString((UInt64)propertyValue);
                case StorageType.Uri:
                    return ((Uri)propertyValue).ToString();
                case StorageType.XDocument:
                    return ((System.Xml.Linq.XDocument)propertyValue).ToString();
                case StorageType.XElement:
                    return ((System.Xml.Linq.XElement)propertyValue).ToString();
#if !ASTORIA_LIGHT // System.Data.Linq not available
                case StorageType.Binary:
                    Debug.Assert(null != knownTypes[(int)StorageType.Binary], "null typeof(System.Data.Linq.Binary)");
                    Debug.Assert(knownTypes[(int)StorageType.Binary].IsInstanceOfType(propertyValue), "not IsInstanceOfType System.Data.Linq.Binary");
                    return propertyValue.ToString();
#endif
                default:
                    Debug.Assert(false, "new StorageType without update to knownTypes");
                    return propertyValue.ToString();
            }
        }

        /// <summary>
        /// Is this a known primitive type (including string,byte[],uri)
        /// </summary>
        /// <param name="type">type to analyze</param>
        /// <returns>true if known primitive type</returns>
        internal static bool IsKnownType(Type type)
        {
            return (0 <= IndexOfStorage(type));
        }

        /// <summary>
        /// Is this a known primitive type or a nullable based on a primitive type (including string,byte[],uri)
        /// </summary>
        /// <param name="type">type to analyze, possibly nullable</param>
        /// <returns>true if known primitive type or a nullable based on a primitive type</returns>
        internal static bool IsKnownNullableType(Type type)
        {
            return IsKnownType(Nullable.GetUnderlyingType(type) ?? type);
        }

        /// <summary>create list of known value (and reference) types</summary>
        /// <returns>static list of known types matching StorageType enum</returns>
        private static Type[] CreateKnownPrimitives()
        {
#if !ASTORIA_LIGHT // System.Data.Linq not available
            Type[] types = new Type[1 + (int)StorageType.Binary];
#else
            Type[] types = new Type[1 + (int)StorageType.XElement];
#endif
            types[(int)StorageType.Boolean] = typeof(Boolean);
            types[(int)StorageType.Byte] = typeof(Byte);
            types[(int)StorageType.ByteArray] = typeof(Byte[]);
            types[(int)StorageType.Char] = typeof(Char);
            types[(int)StorageType.CharArray] = typeof(Char[]);
            types[(int)StorageType.DateTime] = typeof(DateTime);
            types[(int)StorageType.DateTimeOffset] = typeof(DateTimeOffset);
            types[(int)StorageType.Decimal] = typeof(Decimal);
            types[(int)StorageType.Double] = typeof(Double);
            types[(int)StorageType.Guid] = typeof(Guid);
            types[(int)StorageType.Int16] = typeof(Int16);
            types[(int)StorageType.Int32] = typeof(Int32);
            types[(int)StorageType.Int64] = typeof(Int64);
            types[(int)StorageType.Single] = typeof(Single);
            types[(int)StorageType.String] = typeof(String);
            types[(int)StorageType.SByte] = typeof(SByte);
            types[(int)StorageType.TimeSpan] = typeof(TimeSpan);
            types[(int)StorageType.Type] = typeof(Type);
            types[(int)StorageType.UInt16] = typeof(UInt16);
            types[(int)StorageType.UInt32] = typeof(UInt32);
            types[(int)StorageType.UInt64] = typeof(UInt64);
            types[(int)StorageType.Uri] = typeof(Uri);
            types[(int)StorageType.XDocument] = typeof(System.Xml.Linq.XDocument);
            types[(int)StorageType.XElement] = typeof(System.Xml.Linq.XElement);
#if !ASTORIA_LIGHT // System.Data.Linq not available
            types[(int)StorageType.Binary] = null; // delay populated
#endif
            return types;
        }

        /// <summary>get the StorageType for known types</summary>
        /// <param name="type">type being tested for known type</param>
        /// <returns>-1 or (int)StorageType</returns>
        /// <remarks>will validate System.Data.Linq.Binary on demand</remarks>
        private static int IndexOfStorage(Type type)
        {
            int index = IndexOfReference(ClientConvert.knownTypes, type);

            if ((index < 0) && needSystemDataLinqBinary && (type.Name == "Binary"))
            {
                return LoadSystemDataLinqBinary(type);
            }
            return index;
        }

        /// <summary>
        /// index of value reference in the array
        /// </summary>
        /// <typeparam name="T">generic type</typeparam>
        /// <param name="array">array to search</param>
        /// <param name="value">value being looked for</param>
        /// <returns>index of value reference in the array else (-1)</returns>
        private static int IndexOfReference<T>(T[] array, T value) where T : class
        {
            Debug.Assert(null != array, "null array");
            for (int i = 0; i < array.Length; ++i)
            {
                if (object.ReferenceEquals(array[i], value))
                {
                    return i;
                }
            }

            return -1;
        }

        /// <summary>validating type is System.Data.Linq.Binary</summary>
        /// <param name="type">type to verify its System.Data.Linq.Binary</param>
        /// <returns>-1 or (int)StorageType</returns>
        private static int LoadSystemDataLinqBinary(Type type)
        {
            if ((type.Namespace == "System.Data.Linq") &&
                (System.Reflection.AssemblyName.ReferenceMatchesDefinition(
                    type.Assembly.GetName(), new System.Reflection.AssemblyName(SystemDataLinq))))
            {
                ClientConvert.knownTypes[(int)StorageType.Binary] = type;
                needSystemDataLinqBinary = false;
                return (int)StorageType.Binary;
            }

            return -1;
        }
    }
}
